package org.apache.maven.lifecycle.internal;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * @since 3.0
 * @author Kristian Rosenvold
 *         <p/>
 *         NOTE: This class is not part of any public api and can be changed or deleted without
 *         prior notice. This class in particular may spontaneusly self-combust and be replaced by a
 *         plexus-compliant thread aware logger implementation at any time.
 */
@SuppressWarnings({ "SynchronizationOnLocalVariableOrMethodParameter" })
public class ThreadOutputMuxer {
  private final Iterator<ProjectSegment> projects;

  private final ThreadLocal<ProjectSegment> projectBuildThreadLocal = new ThreadLocal<ProjectSegment>();

  private final Map<ProjectSegment, ByteArrayOutputStream> streams = new HashMap<ProjectSegment, ByteArrayOutputStream>();

  private final Map<ProjectSegment, PrintStream> printStreams = new HashMap<ProjectSegment, PrintStream>();

  private final ByteArrayOutputStream defaultOutputStreamForUnknownData = new ByteArrayOutputStream();

  private final PrintStream defaultPringStream = new PrintStream(defaultOutputStreamForUnknownData);

  private final Set<ProjectSegment> completedBuilds = Collections.synchronizedSet(new HashSet<ProjectSegment>());

  private volatile ProjectSegment currentBuild;

  private final PrintStream originalSystemOUtStream;

  private final ConsolePrinter printer;

  /**
   * A simple but safe solution for printing to the console.
   */

  class ConsolePrinter implements Runnable {
    public volatile boolean running;

    private final ProjectBuildList projectBuildList;

    ConsolePrinter(ProjectBuildList projectBuildList) {
      this.projectBuildList = projectBuildList;
    }

    public void run() {
      running = true;
      for (ProjectSegment projectBuild : projectBuildList) {
        final PrintStream projectStream = printStreams.get(projectBuild);
        ByteArrayOutputStream projectOs = streams.get(projectBuild);

        do {
          synchronized (projectStream) {
            try {
              projectStream.wait(100);
            } catch (InterruptedException e) {
              throw new RuntimeException(e);
            }
            try {
              projectOs.writeTo(originalSystemOUtStream);
            } catch (IOException e) {
              throw new RuntimeException(e);
            }

            projectOs.reset();
          }
        } while (!completedBuilds.contains(projectBuild));
      }
      running = false;
    }

    /*
     * Wait until we are sure the print-stream thread is running.
     */

    public void waitUntilRunning(boolean expect) {
      while (!running == expect) {
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

  public ThreadOutputMuxer(ProjectBuildList segmentChunks, PrintStream originalSystemOut) {
    projects = segmentChunks.iterator();
    for (ProjectSegment segmentChunk : segmentChunks) {
      final ByteArrayOutputStream value = new ByteArrayOutputStream();
      streams.put(segmentChunk, value);
      printStreams.put(segmentChunk, new PrintStream(value));
    }
    setNext();
    this.originalSystemOUtStream = originalSystemOut;
    System.setOut(new ThreadBoundPrintStream(this.originalSystemOUtStream));
    printer = new ConsolePrinter(segmentChunks);
    new Thread(printer).start();
    printer.waitUntilRunning(true);
  }

  public void close() {
    printer.waitUntilRunning(false);
    System.setOut(this.originalSystemOUtStream);
  }

  private void setNext() {
    currentBuild = projects.hasNext() ? projects.next() : null;
  }

  private boolean ownsRealOutputStream(ProjectSegment projectBuild) {
    return projectBuild.equals(currentBuild);
  }

  private PrintStream getThreadBoundPrintStream() {
    ProjectSegment threadProject = projectBuildThreadLocal.get();
    if (threadProject == null) {
      return defaultPringStream;
    }
    if (ownsRealOutputStream(threadProject)) {
      return originalSystemOUtStream;
    }
    return printStreams.get(threadProject);
  }

  public void associateThreadWithProjectSegment(ProjectSegment projectBuild) {
    projectBuildThreadLocal.set(projectBuild);
  }

  public void setThisModuleComplete(ProjectSegment projectBuild) {
    completedBuilds.add(projectBuild);
    PrintStream stream = printStreams.get(projectBuild);
    synchronized (stream) {
      stream.notifyAll();
    }
    disconnectThreadFromProject();
  }

  private void disconnectThreadFromProject() {
    projectBuildThreadLocal.remove();
  }

  private class ThreadBoundPrintStream extends PrintStream {

    public ThreadBoundPrintStream(PrintStream systemOutStream) {
      super(systemOutStream);
    }

    private PrintStream getOutputStreamForCurrentThread() {
      return getThreadBoundPrintStream();
    }

    @Override
    public void println() {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println();
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(char c) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(c);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(char x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(double d) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(d);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(double x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(float f) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(f);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(float x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(int i) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(i);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(int x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(long l) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(l);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(long x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(boolean b) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(b);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(boolean x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(char s[]) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(s);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(char x[]) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(Object obj) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(obj);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(Object x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void print(String s) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.print(s);
        currentStream.notifyAll();
      }
    }

    @Override
    public void println(String x) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.println(x);
        currentStream.notifyAll();
      }
    }

    @Override
    public void write(byte b[], int off, int len) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.write(b, off, len);
        currentStream.notifyAll();
      }
    }

    @Override
    public void close() {
      getOutputStreamForCurrentThread().close();
    }

    @Override
    public void flush() {
      getOutputStreamForCurrentThread().flush();
    }

    @Override
    public void write(int b) {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.write(b);
        currentStream.notifyAll();
      }
    }

    @Override
    public void write(byte b[]) throws IOException {
      final PrintStream currentStream = getOutputStreamForCurrentThread();
      synchronized (currentStream) {
        currentStream.write(b);
        currentStream.notifyAll();
      }
    }
  }
}
